SPA Cheatsheet

Here is a quick summary of the components and their uses that are available in the SPA system

Making a SPA Model

You always have to start like this:


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:

Making neurons to represent a Semantic Pointer

If I want to have a group of neurons whose value is a semantic pointer, make a Buffer.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.value = spa.Buffer(dimensions=32)

To probe data from the buffer, use nengo.Probe(model.<name_of_buffer>.state.output)


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.value = spa.Buffer(dimensions=32)
    nengo.Probe(model.value.state.output)

The number of dimensions must be a multiple of 16. If you want to change this, you can set the parameter subdimensions


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.value = spa.Buffer(dimensions=30, subdimensions=10)
    nengo.Probe(model.value.state.output)

You can also set the number of neurons per dimension (the default is 50)


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.value = spa.Buffer(dimensions=32, neurons_per_dimension=10)
    nengo.Probe(model.value.state.output)

Providing input

In addition to providing input using the JavaViz visualizer, you can also manually specify inputs. Do this by defining a function that returns the semantic pointer that will be used as input. Then create a spa.Input object and indicate which Buffer (or other SPA object) should receive that input.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.value = spa.Buffer(dimensions=32)
    nengo.Probe(model.value.state.output)
    
    def my_input(t):
        if t < 0.1:
            return 'CAT'
        elif 0.1 < t < 0.2:
            return 'DOG'
        else:
            return '0'
    model.input = spa.Input(value=my_input)

Making a Memory

A Memory is the same as a Buffer, except that it should continue to store a value even after you remove the input. This is implemented as an Integrator


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.memory = spa.Memory(dimensions=32)

You can probe it in the same way as the Buffer.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.memory = spa.Memory(dimensions=32)
    nengo.Probe(model.memory.state.output)

The Memory has the same parameters as the Buffer. In addition, you can set the time constant of the neurotransmitter bit setting synapse (default of 0.01 seconds). Larger values should give a more stable memory.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.memory = spa.Memory(dimensions=32, synapse=0.1)
    nengo.Probe(model.memory.state.output)

Finally, you can set the time constant of the memory. With tau=None (which is the default), it will attempt to store the memory forever. For other values, the memory should decay over that time.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.memory = spa.Memory(dimensions=32, synapse=0.1, tau=1.0)
    nengo.Probe(model.memory.state.output)

Action Selection

To combine these components into a model, we can define actions, build a Basal Ganglia, and a Thalamus.

Actions have two parts: utility of the action (how good it is given the current state) and the effect of the action (what should happen when the action is selected).

Action utility is often of the form dot(buffer, CAT), which says that the utility is the dot product of the value stored in buffer with the semantic pointer for CAT.

Action effects are often of the form buffer=DOG, which says to send the value DOG to the SPA component buffer. Another common type of action is buffer=other_buffer, which takes the value from one system and sends it to another.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                'dot(buffer1, CAT) --> buffer2=DOG',
                'dot(buffer1, DOG) --> buffer2=HORSE',
                'dot(buffer1, HORSE) --> buffer2=buffer1',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)

Two useful things to probe in this system are model.bg.input, which is the $Q$ values being fed into the basal ganglia, and model.thalamus.actions.output, which indicates the selected action.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                'dot(buffer1, CAT) --> buffer2=DOG',
                'dot(buffer1, DOG) --> buffer2=HORSE',
                'dot(buffer1, HORSE) --> buffer2=buffer1',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)
    
    nengo.Probe(model.bg.input)
    nengo.Probe(model.thalamus.actions.output)

You can add together multiple parts for the utility, and you can also multiply by scalars.

Importantly, try to make sure that the largest utility value is around 1.0, since the neurons don't do a good job representing above that.


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                '(dot(buffer1, CAT) + dot(buffer2, DOG))*0.5 --> buffer2=DOG',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)
    
    nengo.Probe(model.bg.input)
    nengo.Probe(model.thalamus.actions.output)

You can also use complex semantic pointers


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                'dot(buffer1, CAT+DOG*FURRY+~BLUE) --> buffer2=DOG',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)
    
    nengo.Probe(model.bg.input)
    nengo.Probe(model.thalamus.actions.output)

You can even just have a fixed value for the utility


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                '0.5 --> buffer2=DOG',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)
    
    nengo.Probe(model.bg.input)
    nengo.Probe(model.thalamus.actions.output)

For actions, you can have multiple effects by listing them


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    model.buffer3 = spa.Buffer(dimensions=32)
    model.buffer4 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                'dot(buffer1, CAT) --> buffer2=DOG, buffer3=HOUSE, buffer4=CAR',
                'dot(buffer1, DOG) --> buffer2=HORSE',
                'dot(buffer1, HORSE) --> buffer2=buffer1',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)

You can also transform the semantic pointer while routing it


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    actions = spa.Actions(
                'dot(buffer1, CAT) --> buffer2=buffer1*ANIMAL',
                'dot(buffer1, DOG) --> buffer2=buffer1*ANIMAL',
                'dot(buffer1, HORSE) --> buffer2=buffer1*buffer2',
                )
    model.bg = spa.BasalGanglia(actions)
    model.thalamus = spa.Thalamus(model.bg)

Cortical Effects

You can also implement effects that are constant. That is, these are actions that should happen all the time, so we call these Cortical actions. This forms a direct hard-wird connection between two SPA components. For example, we can set one buffer to always take the value from another buffer convolved with a particular semantic pointer


In [ ]:
import nengo
import nengo.spa as spa

model = spa.SPA(label="My Model Name")
with model:
    model.buffer1 = spa.Buffer(dimensions=32)
    model.buffer2 = spa.Buffer(dimensions=32)
    
    nengo.Probe(model.buffer1.state.output)
    nengo.Probe(model.buffer2.state.output)
    
    cortical_actions = spa.Actions(
                'buffer2=buffer1*ANIMAL'
                )
    model.cortical = spa.Cortical(cortical_actions)